/*
**************************************************************************************
*Myriad Engine                                                                       *
*Copyright (C) 2006-2007, 5d Studios (www.5d-Studios.com)                            *
*                                                                                    *
*This library is free software; you can redistribute it and/or                       *
*modify it under the terms of the GNU Lesser General Public                          *
*License as published by the Free Software Foundation; either                        *
*version 2.1 of the License, or (at your option) any later version.                  *
*                                                                                    *
*This library is distributed in the hope that it will be useful,                     *
*but WITHOUT ANY WARRANTY; without even the implied warranty of                      *
*MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU                   *
*Lesser General Public License for more details.                                     *
*                                                                                    *
*You should have received a copy of the GNU Lesser General Public                    *
*License along with this library; if not, write to the Free Software                 *
*Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA      *
**************************************************************************************
*/
package leola.frontend;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;

import leola.ast.ASTNode;
import leola.backend.interpreter.Interpreter;
import leola.backend.interpreter.LeoTypeConverter;
import leola.backend.interpreter.Scope;
import leola.frontend.events.ParserSummaryEvent;
import leola.frontend.events.ParserSummaryListener;
import leola.frontend.events.SourceLineEvent;
import leola.frontend.events.SourceLineListener;
import leola.frontend.events.SyntaxErrorEvent;
import leola.frontend.events.SyntaxErrorListener;
import leola.frontend.listener.EventDispatcher;
import leola.frontend.tokens.LeolaErrorCode;
import leola.lang.IOLeolaLibrary;
import leola.lang.SystemLeolaLibrary;
import leola.lang.collection.CollectionsLeolaLibrary;
import leola.types.LeoObject;
import leola.utils.InteractiveReader;

/**
 * The {@link Leola} main entry point.
 *
 * @author Tony
 *
 */
public class Leola {

	/**
	 * @param args
	 */
	public static void main(String[] args) throws Exception {
		Leola leola = new Leola();
		if ( args.length > 0 ) {
			for(String arg : args) {
				if ( "-i".equalsIgnoreCase(arg)) {
					leola.startInteractive();
				}
				else {
					leola.evalFile(arg);
				}
			}
		}

		leola.evalFile("./scripts/test-interfaces.leola");

//		leola.evalFile("./scripts/frame-demo.leola");
//		leola.evalFile("./scripts/tree-algorithms.leola");

//		leola.evalFile("./scripts/collections.leola");

//		leola.evalFile("./scripts/test-binary-tree.leola");
//		leola.evalFile("./scripts/test-io-lib.leola");
//		leola.evalFile("./scripts/test-threads.leola");
//		leola.evalFile("./scripts/test-mandelbrot.leola");
//		leola.evalFile("./scripts/test-conditionalbug.leola");
//		leola.evalFile("./scripts/test-arrays.leola");
//		leola.evalFile("./scripts/test-classes.leola");
//		leola.evalFile("./scripts/test-heap.leola");
//		leola.evalFile("./scripts/test-binaryops.leola");
//		leola.evalFile("./scripts/test-closures.leola");
//		leola.evalFile("./scripts/test.leola");
//		leola.evalFile("./scripts/test-native.leola");
//		leola.startInteractive();
	}

	/**
	 * Header
	 */
	private static final String HEADER = "Leola Programming Language. \nBy: Tony Sparks \n";

	/**
	 * {@link ExceptionHandler}
	 */
	private ExceptionHandler exceptionHandler;

	/**
	 * Event Dispatcher
	 */
	private EventDispatcher eventDispatcher;

	/**
	 * Visitor
	 */
	private Interpreter visitor;

	/**
	 * Global scope
	 */
	private Scope global;

	/**
	 * The local thread interpreter
	 */
	private ThreadLocal<Interpreter> localInterpreter = new ThreadLocal<Interpreter>() {
		protected Interpreter initialValue() {
			if ( visitor == null ) {
				visitor = new Interpreter();
				global = visitor.getSymbolStack().getGlobal();
				return visitor;
			}
			return visitor.shallowClone();
		}

	};

	/**
	 *
	 */
	public Leola() throws Exception {
		this(new StdoutExceptionHandler());
	}

	/**
	 * @param exceptionHandler
	 * @throws Exception
	 */
	public Leola(ExceptionHandler exceptionHandler) throws Exception {
		this.eventDispatcher = new EventDispatcher();

		SourceMessageListener sourceListener = new SourceMessageListener();
		this.eventDispatcher.addEventListener(SourceLineEvent.class, sourceListener);

		ParserMessageListener parserListener = new ParserMessageListener();
		this.eventDispatcher.addEventListener(SyntaxErrorEvent.class, parserListener);
		this.eventDispatcher.addEventListener(ParserSummaryEvent.class, parserListener);

		this.exceptionHandler = exceptionHandler;

		this.visitor = getInterpreter();
		loadLibrary(new SystemLeolaLibrary());
		loadLibrary(new IOLeolaLibrary());
		loadLibrary(new CollectionsLeolaLibrary());

		set("this", this);

	}

	/**
	 * TODO - Implement the interactive console.
	 *
	 * @throws Exception
	 */
	private void startInteractive() throws Exception {
		System.out.println(HEADER);

		java.util.Scanner scanner = new java.util.Scanner(System.in);
		System.out.print("leola % ");
		while( scanner.hasNextLine() ) {
			String output = scanner.nextLine();
			if ( ! output.equals("")) {
				this.eval(new InteractiveReader(output, System.in));
			}
//			else {
//				System.out.println();
//			}
			System.out.print("leola % ");
		}
	}

	/**
	 * @return the top level interpreter
	 */
	public Interpreter getInterpreter() {
		return this.localInterpreter.get();
	}

	/**
	 * Loads a {@link LeolaLibrary}.
	 *
	 * @param lib
	 * @throws Exception
	 */
	public void loadLibrary(LeolaLibrary lib) throws Exception {
		lib.init(this);
	}

	/**
	 * Loads a {@link LeolaLibrary}.
	 *
	 * @param libClass
	 * @throws Exception
	 */
	public void loadLibrary(Class<?> libClass) throws Exception {
		LeolaLibrary lib = (LeolaLibrary)libClass.newInstance();
		loadLibrary(lib);
	}

	/**
	 * Evaluates the file.
	 *
	 * @param fileName
	 * @throws Exception
	 */
	public void evalFile(String fileName) throws Exception {
		evalFile(new File(fileName));
	}

	/**
	 * Evaluates the file.
	 *
	 * @param file
	 * @throws Exception
	 */
	public void evalFile(File file) throws Exception {
		eval(new BufferedReader(new FileReader(file)));
	}

	/**
	 * Reads in the inline source.
	 *
	 * @param inlineSource
	 * @throws Exception
	 */
	public void eval(String inlineSource) throws Exception {
		eval(new BufferedReader(new StringReader(inlineSource)));
	}

	/**
	 * Evaluate the stream.
	 *
	 * @param iStream
	 * @throws Exception
	 */
	public void eval(InputStream iStream) throws Exception {
		eval(new BufferedReader(new InputStreamReader(iStream)) );
	}

	/**
	 * Evaluate the stream.
	 *
	 * @param reader
	 * @throws Exception
	 */
	public void eval(BufferedReader reader) throws Exception {
		Source source = new Source(this.eventDispatcher, reader);

		Scanner scanner = new LeolaScanner(source);
		Parser parser = new LeolaParser(scanner, this.exceptionHandler);

		try {
			ASTNode program = parser.parse();
			program.visit(this.visitor);
		}
		catch(Exception e) {
			this.exceptionHandler.onException(e);
		}
		catch(Throwable e) {
			this.exceptionHandler.onException(new Exception(e));
		}
	}

	/**
	 * Place a variable for access by scripts.
	 *
	 * @param variableName
	 * @param value
	 */
	public void put(String variableName, Object value) throws Exception {
		this.global.getSymbolTable().put(variableName, LeoTypeConverter.convertToLeolaType(value));
	}

	/**
	 * Sets a variable - the variable must be defined.
	 *
	 * @param variableName
	 * @param value
	 * @throws Exception
	 */
	public void set(String variableName, Object value) throws Exception {
		this.global.getSymbolTable().set(variableName, LeoTypeConverter.convertToLeolaType(value));
	}

	/**
	 * Returns the reference to an object.
	 * @param variableName
	 * @return null if not found.
	 * @throws Exception
	 */
	public Object get(String variableName) throws Exception {
		LeoObject obj = this.global.getSymbolTable().get(variableName);
		return (obj != null ) ? obj.getValue() : null;
	}

	/**
	 *
	 * @author Tony
	 *
	 */
	private static class StdoutExceptionHandler implements ExceptionHandler {

		/* (non-Javadoc)
		 * @see leola.frontend.ExceptionHandler#errorToken(leola.frontend.Token, leola.frontend.Parser, leola.frontend.tokens.LeolaErrorCode)
		 */
		@Override
		public void errorToken(Token token, Parser parser,
				LeolaErrorCode errorCode) {
			System.err.println(">>> " + errorCode + " at line: " + token.getLineNumber() + " for " + token.getText());
		}

		/* (non-Javadoc)
		 * @see leola.frontend.ExceptionHandler#onException(java.lang.Exception)
		 */
		@Override
		public void onException(Exception e) {
			System.err.println(e);
		}

	}

	private static final String SOURCE_LINE_FORMAT = "%03d %s";

	/**
	 * wether or not there is a syntax error
	 */
//	private boolean hasSyntaxError = false;

	private boolean printSource = false;

    /**
     * Listener for source messages.
     */
    private class SourceMessageListener implements SourceLineListener
    {
        /**
         * Called by the source whenever it produces a message.
         * @param message the message.
         */
        public void onEvent(leola.frontend.events.SourceLineEvent event)
        {
        	if ( printSource ) {
	        	System.out.println(String.format(SOURCE_LINE_FORMAT,
	                    event.getLineNumber(), event.getLine()));
        	}
        }
    }

    private static final String PARSER_SUMMARY_FORMAT =
        "\n%,20d source lines." +
        "\n%,20d syntax errors." +
        "\n%,20.2f seconds total parsing time.\n";

    private static final int PREFIX_WIDTH = 5;

    /**
     * Listener for parser messages.
     */
    private class ParserMessageListener
    	implements SyntaxErrorListener
    			  , ParserSummaryListener
    {
    	/* (non-Javadoc)
    	 * @see leola.frontend.events.ParserSummaryListener#onEvent(leola.frontend.events.ParserSummaryEvent)
    	 */
    	@Override
    	public void onEvent(ParserSummaryEvent evnt) {
    		if ( printSource ) {
	            System.out.printf(PARSER_SUMMARY_FORMAT,
	                    evnt.getTotalLines(), evnt.getErrorCount(),
	                    evnt.getElapsedTime());
    		}
    	}

    	/* (non-Javadoc)
    	 * @see leola.frontend.events.SyntaxErrorListener#onEvent(leola.frontend.events.SyntaxErrorEvent)
    	 */
    	@Override
    	public void onEvent(SyntaxErrorEvent event) {
//            int lineNumber = event.getLineNumber();
            int position = event.getPosition();
            String tokenText = event.getTokenText();
            String errorMessage = event.getErrorMessage();

            int spaceCount = PREFIX_WIDTH + position;
            StringBuilder flagBuffer = new StringBuilder();

            // Spaces up to the error position.
            for (int i = 1; i < spaceCount; ++i) {
                flagBuffer.append(' ');
            }

            // A pointer to the error followed by the error message.
            flagBuffer.append("^\n*** ").append(errorMessage);

            // Text, if any, of the bad token.
            if (tokenText != null) {
                flagBuffer.append(" [at \"").append(tokenText)
                    .append("\"]");
            }

            System.out.println(flagBuffer.toString());
    	}
    }
}
